// Astring is a tiny and fast JavaScript code generator from an ESTree-compliant AST.
//
// Astring was written by David Bonnet and released under an MIT license.
//
// The Git repository for Astring is available at:
// https://github.com/davidbonnet/astring.git
//
// Please use the GitHub bug tracker to report issues:
// https://github.com/davidbonnet/astring/issues

const { stringify } = JSON

/* istanbul ignore if */
if (!String.prototype.repeat) {
    /* istanbul ignore next */
    throw new Error(
        'String.prototype.repeat is undefined, see https://github.com/davidbonnet/astring#installation',
    )
}

/* istanbul ignore if */
if (!String.prototype.endsWith) {
    /* istanbul ignore next */
    throw new Error(
        'String.prototype.endsWith is undefined, see https://github.com/davidbonnet/astring#installation',
    )
}

const OPERATOR_PRECEDENCE = {
    '||': 3,
    '&&': 4,
    '|': 5,
    '^': 6,
    '&': 7,
    '==': 8,
    '!=': 8,
    '===': 8,
    '!==': 8,
    '<': 9,
    '>': 9,
    '<=': 9,
    '>=': 9,
    in: 9,
    instanceof: 9,
    '<<': 10,
    '>>': 10,
    '>>>': 10,
    '+': 11,
    '-': 11,
    '*': 12,
    '%': 12,
    '/': 12,
    '**': 13,
}

// Enables parenthesis regardless of precedence
const NEEDS_PARENTHESES = 17

const EXPRESSIONS_PRECEDENCE = {
    // Definitions
    ArrayExpression: 20,
    TaggedTemplateExpression: 20,
    ThisExpression: 20,
    Identifier: 20,
    Literal: 18,
    TemplateLiteral: 20,
    Super: 20,
    SequenceExpression: 20,
    // Operations
    MemberExpression: 19,
    CallExpression: 19,
    NewExpression: 19,
    // Other definitions
    ArrowFunctionExpression: NEEDS_PARENTHESES,
    ClassExpression: NEEDS_PARENTHESES,
    FunctionExpression: NEEDS_PARENTHESES,
    ObjectExpression: NEEDS_PARENTHESES,
    // Other operations
    UpdateExpression: 16,
    UnaryExpression: 15,
    BinaryExpression: 14,
    LogicalExpression: 13,
    ConditionalExpression: 4,
    AssignmentExpression: 3,
    AwaitExpression: 2,
    YieldExpression: 2,
    RestElement: 1,
}

function formatSequence(state, nodes) {
    /*
    Writes into `state` a sequence of `nodes`.
    */
    const { generator } = state
    state.write('(')
    if (nodes != null && nodes.length > 0) {
        generator[nodes[0].type](nodes[0], state)
        const { length } = nodes
        for (let i = 1; i < length; i++) {
            const param = nodes[i]
            state.write(', ')
            generator[param.type](param, state)
        }
    }
    state.write(')')
}

function expressionNeedsParenthesis(node, parentNode, isRightHand) {
    const nodePrecedence = EXPRESSIONS_PRECEDENCE[node.type]
    if (nodePrecedence === NEEDS_PARENTHESES) {
        return true
    }
    const parentNodePrecedence = EXPRESSIONS_PRECEDENCE[parentNode.type]
    if (nodePrecedence !== parentNodePrecedence) {
        // Different node types
        return (
            (!isRightHand &&
                nodePrecedence === 15 &&
                parentNodePrecedence === 14 &&
                parentNode.operator === '**') ||
            nodePrecedence < parentNodePrecedence
        )
    }
    if (nodePrecedence !== 13 && nodePrecedence !== 14) {
        // Not a `LogicalExpression` or `BinaryExpression`
        return false
    }
    if (node.operator === '**' && parentNode.operator === '**') {
        // Exponentiation operator has right-to-left associativity
        return !isRightHand
    }
    if (isRightHand) {
        // Parenthesis are used if both operators have the same precedence
        return (
            OPERATOR_PRECEDENCE[node.operator] <=
            OPERATOR_PRECEDENCE[parentNode.operator]
        )
    }
    return (
        OPERATOR_PRECEDENCE[node.operator] <
        OPERATOR_PRECEDENCE[parentNode.operator]
    )
}

function formatBinaryExpressionPart(state, node, parentNode, isRightHand) {
    /*
    Writes into `state` a left-hand or right-hand expression `node`
    from a binary expression applying the provided `operator`.
    The `isRightHand` parameter should be `true` if the `node` is a right-hand argument.
    */
    const { generator } = state
    if (expressionNeedsParenthesis(node, parentNode, isRightHand)) {
        state.write('(')
        generator[node.type](node, state)
        state.write(')')
    } else {
        generator[node.type](node, state)
    }
}

function reindent(state, text, indent, lineEnd) {
    /*
    Writes into `state` the `text` string reindented with the provided `indent`.
    */
    const lines = text.split('\n')
    const end = lines.length - 1
    state.write(lines[0].trim())
    if (end > 0) {
        state.write(lineEnd)
        for (let i = 1; i < end; i++) {
            state.write(indent + lines[i].trim() + lineEnd)
        }
        state.write(indent + lines[end].trim())
    }
}

function formatComments(state, comments, indent, lineEnd) {
    /*
    Writes into `state` the provided list of `comments`, with the given `indent` and `lineEnd` strings.
    Line comments will end with `"\n"` regardless of the value of `lineEnd`.
    Expects to start on a new unindented line.
    */
    const { length } = comments
    for (let i = 0; i < length; i++) {
        const comment = comments[i]
        state.write(indent)
        if (comment.type[0] === 'L') {
            // Line comment
            state.write('// ' + comment.value.trim() + '\n')
        } else {
            // Block comment
            state.write('/*')
            reindent(state, comment.value, indent, lineEnd)
            state.write('*/' + lineEnd)
        }
    }
}

function hasCallExpression(node) {
    /*
    Returns `true` if the provided `node` contains a call expression and `false` otherwise.
    */
    let currentNode = node
    while (currentNode != null) {
        const { type } = currentNode
        if (type[0] === 'C' && type[1] === 'a') {
            // Is CallExpression
            return true
        } else if (type[0] === 'M' && type[1] === 'e' && type[2] === 'm') {
            // Is MemberExpression
            currentNode = currentNode.object
        } else {
            return false
        }
    }
}

function formatVariableDeclaration(state, node) {
    /*
    Writes into `state` a variable declaration.
    */
    const { generator } = state
    const { declarations } = node
    state.write(node.kind + ' ')
    const { length } = declarations
    if (length > 0) {
        generator.VariableDeclarator(declarations[0], state)
        for (let i = 1; i < length; i++) {
            state.write(', ')
            generator.VariableDeclarator(declarations[i], state)
        }
    }
}

let ForInStatement,
    FunctionDeclaration,
    RestElement,
    BinaryExpression,
    ArrayExpression,
    BlockStatement

const baseGenerator = {
    Program(node, state) {
        const indent = state.indent.repeat(state.indentLevel)
        const { lineEnd, writeComments } = state
        if (writeComments && node.comments != null) {
            formatComments(state, node.comments, indent, lineEnd)
        }
        const statements = node.body
        const { length } = statements
        for (let i = 0; i < length; i++) {
            const statement = statements[i]
            if (writeComments && statement.comments != null) {
                formatComments(state, statement.comments, indent, lineEnd)
            }
            state.write(indent)
            this[statement.type](statement, state)
            state.write(lineEnd)
        }
        if (writeComments && node.trailingComments != null) {
            formatComments(state, node.trailingComments, indent, lineEnd)
        }
    },
    BlockStatement: (BlockStatement = function(node, state) {
        const indent = state.indent.repeat(state.indentLevel++)
        const { lineEnd, writeComments } = state
        const statementIndent = indent + state.indent
        state.write('{')
        const statements = node.body
        if (statements != null && statements.length > 0) {
            state.write(lineEnd)
            if (writeComments && node.comments != null) {
                formatComments(state, node.comments, statementIndent, lineEnd)
            }
            const { length } = statements
            for (let i = 0; i < length; i++) {
                const statement = statements[i]
                if (writeComments && statement.comments != null) {
                    formatComments(state, statement.comments, statementIndent, lineEnd)
                }
                state.write(statementIndent)
                this[statement.type](statement, state)
                state.write(lineEnd)
            }
            state.write(indent)
        } else {
            if (writeComments && node.comments != null) {
                state.write(lineEnd)
                formatComments(state, node.comments, statementIndent, lineEnd)
                state.write(indent)
            }
        }
        if (writeComments && node.trailingComments != null) {
            formatComments(state, node.trailingComments, statementIndent, lineEnd)
        }
        state.write('}')
        state.indentLevel--
    }),
    ClassBody: BlockStatement,
    EmptyStatement(node, state) {
        state.write(';')
    },
    ExpressionStatement(node, state) {
        const precedence = EXPRESSIONS_PRECEDENCE[node.expression.type]
        if (
            precedence === NEEDS_PARENTHESES ||
            (precedence === 3 && node.expression.left.type[0] === 'O')
        ) {
            // Should always have parentheses or is an AssignmentExpression to an ObjectPattern
            state.write('(')
            this[node.expression.type](node.expression, state)
            state.write(')')
        } else {
            this[node.expression.type](node.expression, state)
        }
        state.write(';')
    },
    IfStatement(node, state) {
        state.write('if (')
        this[node.test.type](node.test, state)
        state.write(') ')
        this[node.consequent.type](node.consequent, state)
        if (node.alternate != null) {
            state.write(' else ')
            this[node.alternate.type](node.alternate, state)
        }
    },
    LabeledStatement(node, state) {
        this[node.label.type](node.label, state)
        state.write(': ')
        this[node.body.type](node.body, state)
    },
    BreakStatement(node, state) {
        state.write('break')
        if (node.label != null) {
            state.write(' ')
            this[node.label.type](node.label, state)
        }
        state.write(';')
    },
    ContinueStatement(node, state) {
        state.write('continue')
        if (node.label != null) {
            state.write(' ')
            this[node.label.type](node.label, state)
        }
        state.write(';')
    },
    WithStatement(node, state) {
        state.write('with (')
        this[node.object.type](node.object, state)
        state.write(') ')
        this[node.body.type](node.body, state)
    },
    SwitchStatement(node, state) {
        const indent = state.indent.repeat(state.indentLevel++)
        const { lineEnd, writeComments } = state
        state.indentLevel++
        const caseIndent = indent + state.indent
        const statementIndent = caseIndent + state.indent
        state.write('switch (')
        this[node.discriminant.type](node.discriminant, state)
        state.write(') {' + lineEnd)
        const { cases: occurences } = node
        const { length: occurencesCount } = occurences
        for (let i = 0; i < occurencesCount; i++) {
            const occurence = occurences[i]
            if (writeComments && occurence.comments != null) {
                formatComments(state, occurence.comments, caseIndent, lineEnd)
            }
            if (occurence.test) {
                state.write(caseIndent + 'case ')
                this[occurence.test.type](occurence.test, state)
                state.write(':' + lineEnd)
            } else {
                state.write(caseIndent + 'default:' + lineEnd)
            }
            const { consequent } = occurence
            const { length: consequentCount } = consequent
            for (let i = 0; i < consequentCount; i++) {
                const statement = consequent[i]
                if (writeComments && statement.comments != null) {
                    formatComments(state, statement.comments, statementIndent, lineEnd)
                }
                state.write(statementIndent)
                this[statement.type](statement, state)
                state.write(lineEnd)
            }
        }
        state.indentLevel -= 2
        state.write(indent + '}')
    },
    ReturnStatement(node, state) {
        state.write('return')
        if (node.argument) {
            state.write(' ')
            this[node.argument.type](node.argument, state)
        }
        state.write(';')
    },
    ThrowStatement(node, state) {
        state.write('throw ')
        this[node.argument.type](node.argument, state)
        state.write(';')
    },
    TryStatement(node, state) {
        state.write('try ')
        this[node.block.type](node.block, state)
        if (node.handler) {
            const { handler } = node
            if (handler.param == null) {
                state.write(' catch ')
            } else {
                state.write(' catch (')
                this[handler.param.type](handler.param, state)
                state.write(') ')
            }
            this[handler.body.type](handler.body, state)
        }
        if (node.finalizer) {
            state.write(' finally ')
            this[node.finalizer.type](node.finalizer, state)
        }
    },
    WhileStatement(node, state) {
        state.write('while (')
        this[node.test.type](node.test, state)
        state.write(') ')
        this[node.body.type](node.body, state)
    },
    DoWhileStatement(node, state) {
        state.write('do ')
        this[node.body.type](node.body, state)
        state.write(' while (')
        this[node.test.type](node.test, state)
        state.write(');')
    },
    ForStatement(node, state) {
        state.write('for (')
        if (node.init != null) {
            const { init } = node
            if (init.type[0] === 'V') {
                formatVariableDeclaration(state, init)
            } else {
                this[init.type](init, state)
            }
        }
        state.write('; ')
        if (node.test) {
            this[node.test.type](node.test, state)
        }
        state.write('; ')
        if (node.update) {
            this[node.update.type](node.update, state)
        }
        state.write(') ')
        this[node.body.type](node.body, state)
    },
    ForInStatement: (ForInStatement = function(node, state) {
        state.write(`for ${node.await ? 'await ' : ''}(`)
        const { left } = node
        if (left.type[0] === 'V') {
            formatVariableDeclaration(state, left)
        } else {
            this[left.type](left, state)
        }
        // Identifying whether node.type is `ForInStatement` or `ForOfStatement`
        state.write(node.type[3] === 'I' ? ' in ' : ' of ')
        this[node.right.type](node.right, state)
        state.write(') ')
        this[node.body.type](node.body, state)
    }),
    ForOfStatement: ForInStatement,
    DebuggerStatement(node, state) {
        state.write('debugger;' + state.lineEnd)
    },
    FunctionDeclaration: (FunctionDeclaration = function(node, state) {
        state.write(
            (node.async ? 'async ' : '') +
            (node.generator ? 'function* ' : 'function ') +
            (node.id ? node.id.name : ''),
            node,
        )
        formatSequence(state, node.params)
        state.write(' ')
        this[node.body.type](node.body, state)
    }),
    FunctionExpression: FunctionDeclaration,
    VariableDeclaration(node, state) {
        formatVariableDeclaration(state, node)
        state.write(';')
    },
    VariableDeclarator(node, state) {
        this[node.id.type](node.id, state)
        if (node.init != null) {
            state.write(' = ')
            this[node.init.type](node.init, state)
        }
    },
    ClassDeclaration(node, state) {
        state.write('class ' + (node.id ? `${node.id.name} ` : ''), node)
        if (node.superClass) {
            state.write('extends ')
            this[node.superClass.type](node.superClass, state)
            state.write(' ')
        }
        this.ClassBody(node.body, state)
    },
    ImportDeclaration(node, state) {
        state.write('import ')
        const { specifiers } = node
        const { length } = specifiers
        // NOTE: Once babili is fixed, put this after condition
        // https://github.com/babel/babili/issues/430
        let i = 0
        if (length > 0) {
            for (; i < length; ) {
                if (i > 0) {
                    state.write(', ')
                }
                const specifier = specifiers[i]
                const type = specifier.type[6]
                if (type === 'D') {
                    // ImportDefaultSpecifier
                    state.write(specifier.local.name, specifier)
                    i++
                } else if (type === 'N') {
                    // ImportNamespaceSpecifier
                    state.write('* as ' + specifier.local.name, specifier)
                    i++
                } else {
                    // ImportSpecifier
                    break
                }
            }
            if (i < length) {
                state.write('{')
                for (;;) {
                    const specifier = specifiers[i]
                    const { name } = specifier.imported
                    state.write(name, specifier)
                    if (name !== specifier.local.name) {
                        state.write(' as ' + specifier.local.name)
                    }
                    if (++i < length) {
                        state.write(', ')
                    } else {
                        break
                    }
                }
                state.write('}')
            }
            state.write(' from ')
        }
        this.Literal(node.source, state)
        state.write(';')
    },
    ExportDefaultDeclaration(node, state) {
        state.write('export default ')
        this[node.declaration.type](node.declaration, state)
        if (
            EXPRESSIONS_PRECEDENCE[node.declaration.type] &&
            node.declaration.type[0] !== 'F'
        ) {
            // All expression nodes except `FunctionExpression`
            state.write(';')
        }
    },
    ExportNamedDeclaration(node, state) {
        state.write('export ')
        if (node.declaration) {
            this[node.declaration.type](node.declaration, state)
        } else {
            state.write('{')
            const { specifiers } = node,
                { length } = specifiers
            if (length > 0) {
                for (let i = 0; ; ) {
                    const specifier = specifiers[i]
                    const { name } = specifier.local
                    state.write(name, specifier)
                    if (name !== specifier.exported.name) {
                        state.write(' as ' + specifier.exported.name)
                    }
                    if (++i < length) {
                        state.write(', ')
                    } else {
                        break
                    }
                }
            }
            state.write('}')
            if (node.source) {
                state.write(' from ')
                this.Literal(node.source, state)
            }
            state.write(';')
        }
    },
    ExportAllDeclaration(node, state) {
        state.write('export * from ')
        this.Literal(node.source, state)
        state.write(';')
    },
    MethodDefinition(node, state) {
        if (node.static) {
            state.write('static ')
        }
        const kind = node.kind[0]
        if (kind === 'g' || kind === 's') {
            // Getter or setter
            state.write(node.kind + ' ')
        }
        if (node.value.async) {
            state.write('async ')
        }
        if (node.value.generator) {
            state.write('*')
        }
        if (node.computed) {
            state.write('[')
            this[node.key.type](node.key, state)
            state.write(']')
        } else {
            this[node.key.type](node.key, state)
        }
        formatSequence(state, node.value.params)
        state.write(' ')
        this[node.value.body.type](node.value.body, state)
    },
    ClassExpression(node, state) {
        this.ClassDeclaration(node, state)
    },
    ArrowFunctionExpression(node, state) {
        state.write(node.async ? 'async ' : '', node)
        const { params } = node
        if (params != null) {
            // Omit parenthesis if only one named parameter
            if (params.length === 1 && params[0].type[0] === 'I') {
                // If params[0].type[0] starts with 'I', it can't be `ImportDeclaration` nor `IfStatement` and thus is `Identifier`
                state.write(params[0].name, params[0])
            } else {
                formatSequence(state, node.params)
            }
        }
        state.write(' => ')
        if (node.body.type[0] === 'O') {
            // Body is an object expression
            state.write('(')
            this.ObjectExpression(node.body, state)
            state.write(')')
        } else {
            this[node.body.type](node.body, state)
        }
    },
    ThisExpression(node, state) {
        state.write('this', node)
    },
    Super(node, state) {
        state.write('super', node)
    },
    RestElement: (RestElement = function(node, state) {
        state.write('...')
        this[node.argument.type](node.argument, state)
    }),
    SpreadElement: RestElement,
    YieldExpression(node, state) {
        state.write(node.delegate ? 'yield*' : 'yield')
        if (node.argument) {
            state.write(' ')
            this[node.argument.type](node.argument, state)
        }
    },
    AwaitExpression(node, state) {
        state.write('await ')
        if (node.argument) {
            this[node.argument.type](node.argument, state)
        }
    },
    TemplateLiteral(node, state) {
        const { quasis, expressions } = node
        state.write('`')
        const { length } = expressions
        for (let i = 0; i < length; i++) {
            const expression = expressions[i]
            this.TemplateElement(quasis[i], state)
            state.write('${')
            this[expression.type](expression, state)
            state.write('}')
        }
        state.write(quasis[quasis.length - 1].value.raw)
        state.write('`')
    },
    TemplateElement(node, state) {
        state.write(node.value.raw)
    },
    TaggedTemplateExpression(node, state) {
        this[node.tag.type](node.tag, state)
        this[node.quasi.type](node.quasi, state)
    },
    ArrayExpression: (ArrayExpression = function(node, state) {
        state.write('[')
        if (node.elements.length > 0) {
            const { elements } = node,
                { length } = elements
            for (let i = 0; ; ) {
                const element = elements[i]
                if (element != null) {
                    this[element.type](element, state)
                }
                if (++i < length) {
                    state.write(', ')
                } else {
                    if (element == null) {
                        state.write(', ')
                    }
                    break
                }
            }
        }
        state.write(']')
    }),
    ArrayPattern: ArrayExpression,
    ObjectExpression(node, state) {
        const indent = state.indent.repeat(state.indentLevel++)
        const { lineEnd, writeComments } = state
        const propertyIndent = indent + state.indent
        state.write('{')
        if (node.properties.length > 0) {
            state.write(lineEnd)
            if (writeComments && node.comments != null) {
                formatComments(state, node.comments, propertyIndent, lineEnd)
            }
            const comma = ',' + lineEnd
            const { properties } = node,
                { length } = properties
            for (let i = 0; ; ) {
                const property = properties[i]
                if (writeComments && property.comments != null) {
                    formatComments(state, property.comments, propertyIndent, lineEnd)
                }
                state.write(propertyIndent)
                this[property.type](property, state)
                if (++i < length) {
                    state.write(comma)
                } else {
                    break
                }
            }
            state.write(lineEnd)
            if (writeComments && node.trailingComments != null) {
                formatComments(state, node.trailingComments, propertyIndent, lineEnd)
            }
            state.write(indent + '}')
        } else if (writeComments) {
            if (node.comments != null) {
                state.write(lineEnd)
                formatComments(state, node.comments, propertyIndent, lineEnd)
                if (node.trailingComments != null) {
                    formatComments(state, node.trailingComments, propertyIndent, lineEnd)
                }
                state.write(indent + '}')
            } else if (node.trailingComments != null) {
                state.write(lineEnd)
                formatComments(state, node.trailingComments, propertyIndent, lineEnd)
                state.write(indent + '}')
            } else {
                state.write('}')
            }
        } else {
            state.write('}')
        }
        state.indentLevel--
    },
    Property(node, state) {
        if (node.method || node.kind[0] !== 'i') {
            // Either a method or of kind `set` or `get` (not `init`)
            this.MethodDefinition(node, state)
        } else {
            if (!node.shorthand) {
                if (node.computed) {
                    state.write('[')
                    this[node.key.type](node.key, state)
                    state.write(']')
                } else {
                    this[node.key.type](node.key, state)
                }
                state.write(': ')
            }
            this[node.value.type](node.value, state)
        }
    },
    ObjectPattern(node, state) {
        state.write('{')
        if (node.properties.length > 0) {
            const { properties } = node,
                { length } = properties
            for (let i = 0; ; ) {
                this[properties[i].type](properties[i], state)
                if (++i < length) {
                    state.write(', ')
                } else {
                    break
                }
            }
        }
        state.write('}')
    },
    SequenceExpression(node, state) {
        formatSequence(state, node.expressions)
    },
    UnaryExpression(node, state) {
        if (node.prefix) {
            state.write(node.operator)
            if (node.operator.length > 1) {
                state.write(' ')
            }
            if (
                EXPRESSIONS_PRECEDENCE[node.argument.type] <
                EXPRESSIONS_PRECEDENCE.UnaryExpression
            ) {
                state.write('(')
                this[node.argument.type](node.argument, state)
                state.write(')')
            } else {
                this[node.argument.type](node.argument, state)
            }
        } else {
            // FIXME: This case never occurs
            this[node.argument.type](node.argument, state)
            state.write(node.operator)
        }
    },
    UpdateExpression(node, state) {
        // Always applied to identifiers or members, no parenthesis check needed
        if (node.prefix) {
            state.write(node.operator)
            this[node.argument.type](node.argument, state)
        } else {
            this[node.argument.type](node.argument, state)
            state.write(node.operator)
        }
    },
    AssignmentExpression(node, state) {
        this[node.left.type](node.left, state)
        state.write(' ' + node.operator + ' ')
        this[node.right.type](node.right, state)
    },
    AssignmentPattern(node, state) {
        this[node.left.type](node.left, state)
        state.write(' = ')
        this[node.right.type](node.right, state)
    },
    BinaryExpression: (BinaryExpression = function(node, state) {
        const isIn = node.operator === 'in'
        if (isIn) {
            // Avoids confusion in `for` loops initializers
            state.write('(')
        }
        formatBinaryExpressionPart(state, node.left, node, false)
        state.write(' ' + node.operator + ' ')
        formatBinaryExpressionPart(state, node.right, node, true)
        if (isIn) {
            state.write(')')
        }
    }),
    LogicalExpression: BinaryExpression,
    ConditionalExpression(node, state) {
        if (
            EXPRESSIONS_PRECEDENCE[node.test.type] >
            EXPRESSIONS_PRECEDENCE.ConditionalExpression
        ) {
            this[node.test.type](node.test, state)
        } else {
            state.write('(')
            this[node.test.type](node.test, state)
            state.write(')')
        }
        state.write(' ? ')
        this[node.consequent.type](node.consequent, state)
        state.write(' : ')
        this[node.alternate.type](node.alternate, state)
    },
    NewExpression(node, state) {
        state.write('new ')
        if (
            EXPRESSIONS_PRECEDENCE[node.callee.type] <
            EXPRESSIONS_PRECEDENCE.CallExpression ||
            hasCallExpression(node.callee)
        ) {
            state.write('(')
            this[node.callee.type](node.callee, state)
            state.write(')')
        } else {
            this[node.callee.type](node.callee, state)
        }
        formatSequence(state, node['arguments'])
    },
    CallExpression(node, state) {
        if (
            EXPRESSIONS_PRECEDENCE[node.callee.type] <
            EXPRESSIONS_PRECEDENCE.CallExpression
        ) {
            state.write('(')
            this[node.callee.type](node.callee, state)
            state.write(')')
        } else {
            this[node.callee.type](node.callee, state)
        }
        formatSequence(state, node['arguments'])
    },
    MemberExpression(node, state) {
        if (
            EXPRESSIONS_PRECEDENCE[node.object.type] <
            EXPRESSIONS_PRECEDENCE.MemberExpression
        ) {
            state.write('(')
            this[node.object.type](node.object, state)
            state.write(')')
        } else {
            this[node.object.type](node.object, state)
        }
        if (node.computed) {
            state.write('[')
            this[node.property.type](node.property, state)
            state.write(']')
        } else {
            state.write('.')
            this[node.property.type](node.property, state)
        }
    },
    MetaProperty(node, state) {
        state.write(node.meta.name + '.' + node.property.name, node)
    },
    Identifier(node, state) {
        state.write(node.name, node)
    },
    Literal(node, state) {
        if (node.raw != null) {
            state.write(node.raw, node)
        } else if (node.regex != null) {
            this.RegExpLiteral(node, state)
        } else {
            state.write(stringify(node.value), node)
        }
    },
    RegExpLiteral(node, state) {
        const { regex } = node
        state.write(`/${regex.pattern}/${regex.flags}`, node)
    },
}

const EMPTY_OBJECT = {}

class State {
    constructor(options) {
        const setup = options == null ? EMPTY_OBJECT : options
        this.output = ''
        // Functional options
        if (setup.output != null) {
            this.output = setup.output
            this.write = this.writeToStream
        } else {
            this.output = ''
        }
        this.generator = setup.generator != null ? setup.generator : baseGenerator
        // Formating setup
        this.indent = setup.indent != null ? setup.indent : '  '
        this.lineEnd = setup.lineEnd != null ? setup.lineEnd : '\n'
        this.indentLevel =
            setup.startingIndentLevel != null ? setup.startingIndentLevel : 0
        this.writeComments = setup.comments ? setup.comments : false
        // Source map
        if (setup.sourceMap != null) {
            this.write =
                setup.output == null ? this.writeAndMap : this.writeToStreamAndMap
            this.sourceMap = setup.sourceMap
            this.line = 1
            this.column = 0
            this.lineEndSize = this.lineEnd.split('\n').length - 1
            this.mapping = {
                original: null,
                generated: this,
                name: undefined,
                source: setup.sourceMap.file || setup.sourceMap._file,
            }
        }
    }

    write(code) {
        this.output += code
    }

    writeToStream(code) {
        this.output.write(code)
    }

    writeAndMap(code, node) {
        this.output += code
        this.map(code, node)
    }

    writeToStreamAndMap(code, node) {
        this.output.write(code)
        this.map(code, node)
    }

    map(code, node) {
        if (node != null && node.loc != null) {
            const { mapping } = this
            mapping.original = node.loc.start
            mapping.name = node.name
            this.sourceMap.addMapping(mapping)
        }
        if (code.length > 0) {
            if (this.lineEndSize > 0) {
                if (code.endsWith(this.lineEnd)) {
                    this.line += this.lineEndSize
                    this.column = 0
                } else if (code[code.length - 1] === '\n') {
                    // Case of inline comment
                    this.line++
                    this.column = 0
                } else {
                    this.column += code.length
                }
            } else {
                if (code[code.length - 1] === '\n') {
                    // Case of inline comment
                    this.line++
                    this.column = 0
                } else {
                    this.column += code.length
                }
            }
        }
    }

    toString() {
        return this.output
    }
}

function generate(node, options) {
    /*
    Returns a string representing the rendered code of the provided AST `node`.
    The `options` are:

    - `indent`: string to use for indentation (defaults to `␣␣`)
    - `lineEnd`: string to use for line endings (defaults to `\n`)
    - `startingIndentLevel`: indent level to start from (defaults to `0`)
    - `comments`: generate comments if `true` (defaults to `false`)
    - `output`: output stream to write the rendered code to (defaults to `null`)
    - `generator`: custom code generator (defaults to `baseGenerator`)
    */
    const state = new State(options)
    // Travel through the AST node and generate the code
    state.generator[node.type](node, state)
    return state.output
}